var Gantt = (function () {
  'use strict'

  const YEAR = 'year'
  const MONTH = 'month'
  const DAY = 'day'
  const HOUR = 'hour'
  const MINUTE = 'minute'
  const SECOND = 'second'
  const MILLISECOND = 'millisecond'

  const VIEW_MODE = {
    QUARTER_DAY: 'Quarter Day',
    HALF_DAY: 'Half Day',
    DAY: 'Day',
    WEEK: 'Week',
    MONTH: 'Month',
    YEAR: 'Year',
  }

  var date_utils = {
    parse(date, date_separator = '-', time_separator = /[.:]/) {
      if (date instanceof Date) {
        return date
      }
      if (typeof date === 'string') {
        let date_parts, time_parts
        const parts = date.split(' ')
        date_parts = parts[0]
          .split(date_separator)
          .map((val) => parseInt(val, 10))
        time_parts = parts[1] && parts[1].split(time_separator)
        // month is 0 indexed
        date_parts[1] = date_parts[1] - 1
        let vals = date_parts
        if (time_parts && time_parts.length) {
          if (time_parts.length == 4) {
            time_parts[3] = '0.' + time_parts[3]
            time_parts[3] = parseFloat(time_parts[3]) * 1000
          }
          vals = vals.concat(time_parts)
        }
        return new Date(...vals)
      }
    },

    to_string(date, with_time = false) {
      if (!(date instanceof Date)) {
        throw new TypeError('Invalid argument type')
      }
      const vals = this.get_date_values(date).map((val, i) => {
        if (i === 1) {
          // add 1 for month
          val = val + 1
        }
        if (i === 6) {
          return padStart(val + '', 3, '0')
        }
        return padStart(val + '', 2, '0')
      })
      const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`
      const time_string = `${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}`
      return date_string + (with_time ? ' ' + time_string : '')
    },

    format(date, format_string = 'YYYY-MM-DD HH:mm:ss') {
      const values = this.get_date_values(date).map((d) => padStart(d, 2, 0))
      const format_map = {
        YYYY: values[0],
        MM: padStart(+values[1] + 1, 2, 0),
        DD: values[2],
        HH: values[3],
        mm: values[4],
        ss: values[5],
        SSS: values[6],
      }
      let str = format_string
      const formatted_values = []
      Object.keys(format_map)
        .sort((a, b) => b.length - a.length) // big string first
        .forEach((key) => {
          if (str.includes(key)) {
            str = str.replace(key, `$${formatted_values.length}`)
            formatted_values.push(format_map[key])
          }
        })
      formatted_values.forEach((value, i) => {
        str = str.replace(`$${i}`, value)
      })
      return str
    },

    diff(date_a, date_b, scale = DAY) {
      let milliseconds, seconds, hours, minutes, days, months, years
      milliseconds = date_a - date_b
      seconds = milliseconds / 1000
      minutes = (seconds / 60).toFixed(8)
      hours = (minutes / 60).toFixed(8)
      days = hours / 24
      months = days / 30
      years = months / 12
      if (!scale.endsWith('s')) {
        scale += 's'
      }
      return {
        milliseconds,
        seconds,
        minutes,
        hours,
        days,
        months,
        years,
      }[scale]
    },

    today() {
      const vals = this.get_date_values(new Date()).slice(0, 3)
      return new Date(...vals)
    },

    now() {
      return new Date()
    },

    add(date, qty, scale) {
      qty = parseInt(qty, 10)
      const vals = [
        date.getFullYear() + (scale === YEAR ? qty : 0),
        date.getMonth() + (scale === MONTH ? qty : 0),
        date.getDate() + (scale === DAY ? qty : 0),
        date.getHours() + (scale === HOUR ? qty : 0),
        date.getMinutes() + (scale === MINUTE ? qty : 0),
        date.getSeconds() + (scale === SECOND ? qty : 0),
        date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),
      ]
      return new Date(...vals)
    },

    start_of(date, scale) {
      const scores = {
        [YEAR]: 6,
        [MONTH]: 5,
        [DAY]: 4,
        [HOUR]: 3,
        [MINUTE]: 2,
        [SECOND]: 1,
        [MILLISECOND]: 0,
      }
      function should_reset(_scale) {
        const max_score = scores[scale]
        return scores[_scale] <= max_score
      }
      const vals = [
        date.getFullYear(),
        should_reset(YEAR) ? 0 : date.getMonth(),
        should_reset(MONTH) ? 1 : date.getDate(),
        should_reset(DAY) ? 0 : date.getHours(),
        should_reset(HOUR) ? 0 : date.getMinutes(),
        should_reset(MINUTE) ? 0 : date.getSeconds(),
        should_reset(SECOND) ? 0 : date.getMilliseconds(),
      ]
      return new Date(...vals)
    },

    clone(date) {
      return new Date(...this.get_date_values(date))
    },

    get_date_values(date) {
      return [
        date.getFullYear(),
        date.getMonth(),
        date.getDate(),
        date.getHours(),
        date.getMinutes(),
        date.getSeconds(),
        date.getMilliseconds(),
      ]
    },

    get_days_in_month(date) {
      const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      const month = date.getMonth()
      if (month !== 1) {
        return no_of_days[month]
      }
      const year = date.getFullYear()
      if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
        return 29
      }
      return 28
    },
  }

  function padStart(str, targetLength, padString) {
    str = str + ''
    targetLength = targetLength >> 0
    padString = String(typeof padString !== 'undefined' ? padString : ' ')
    if (str.length > targetLength) {
      return String(str)
    } else {
      targetLength = targetLength - str.length
      if (targetLength > padString.length) {
        padString += padString.repeat(targetLength / padString.length)
      }
      return padString.slice(0, targetLength) + String(str)
    }
  }

  function $(expr, con) {
    return typeof expr === 'string'
      ? (con || document).querySelector(expr)
      : expr || null
  }

  function createSVG(tag, attrs) {
    const elem = document.createElementNS('http://www.w3.org/2000/svg', tag)
    for (let attr in attrs) {
      if (attr === 'append_to') {
        const parent = attrs.append_to
        parent.appendChild(elem)
      } else if (attr === 'innerHTML') {
        elem.innerHTML = attrs.innerHTML
      } else {
        elem.setAttribute(attr, attrs[attr])
      }
    }
    return elem
  }

  $.on = (element, event, selector, callback) => {
    if (!callback) {
      callback = selector
      $.bind(element, event, callback)
    } else {
      $.delegate(element, event, selector, callback)
    }
  }

  $.off = (element, event, handler) => {
    element.removeEventListener(event, handler)
  }

  $.bind = (element, event, callback) => {
    event.split(/\s+/).forEach(function (event) {
      element.addEventListener(event, callback)
    })
  }

  $.delegate = (element, event, selector, callback) => {
    element.addEventListener(event, function (e) {
      const delegatedTarget = e.target.closest(selector)
      if (delegatedTarget) {
        e.delegatedTarget = delegatedTarget
        callback.call(this, e, delegatedTarget)
      }
    })
  }

  $.closest = (selector, element) => {
    if (!element) return null
    if (element.matches(selector)) {
      return element
    }
    return $.closest(selector, element.parentNode)
  }

  $.attr = (element, attr, value) => {
    if (!value && typeof attr === 'string') {
      return element.getAttribute(attr)
    }
    if (typeof attr === 'object') {
      for (let key in attr) {
        $.attr(element, key, attr[key])
      }
      return
    }
    element.setAttribute(attr, value)
  }

  class Bar {
    constructor(gantt, task, index, tasks) {
      this.set_defaults(gantt, task, index, tasks)
      this.prepare()
      this.draw()
      this.bind()
    }

    set_defaults(gantt, task, index, tasks) {
      this.gantt = gantt
      this.task = task
      this.index = index
      this.oldIndex = index
      this.tasks = tasks
    }

    prepare() {
      this.prepare_values()
      this.prepare_helpers()
    }

    prepare_values() {
      this.invalid = this.task.invalid
      this.height = this.gantt.options.bar_height
      this.x = this.compute_x()
      this.y = this.compute_y()
      this.corner_radius = this.gantt.options.bar_corner_radius
      this.duration = (
        date_utils.diff(this.task._end, this.task._start, 'hour') /
        this.gantt.options.step
      ).toFixed(8)
      this.width = this.gantt.options.column_width * this.duration
      this.progress_width =
        this.gantt.options.column_width *
          this.duration *
          (this.task.progress / 100) || 0
      this.group = createSVG('g', {
        class: 'bar-wrapper ' + (this.task.custom_class || ''),
        'data-id': this.task.id,
      })
      this.bar_group = createSVG('g', {
        class: 'bar-group',
        append_to: this.group,
      })

      this.handle_group = createSVG('g', {
        class: 'handle-group',
        append_to: this.group,
      })
    }

    prepare_helpers() {
      SVGElement.prototype.getX = function () {
        return +this.getAttribute('x')
      }
      SVGElement.prototype.getY = function () {
        return +this.getAttribute('y')
      }
      SVGElement.prototype.getWidth = function () {
        return +this.getAttribute('width')
      }
      SVGElement.prototype.getHeight = function () {
        return +this.getAttribute('height')
      }
      SVGElement.prototype.getEndX = function () {
        return this.getX() + this.getWidth()
      }
    }

    draw() {
      this.draw_bar()
      this.draw_label()
      this.draw_resize_handles()
    }

    draw_bar() {
      this.$bar = createSVG('rect', {
        x: this.x,
        y: this.y,
        width: this.width,
        height: this.height,
        rx: this.corner_radius,
        ry: this.corner_radius,
        class: 'bar',
        style: `fill:${
          this.task.selectStatus
            ? this.gantt.options.selectBg || 'red'
            : this.gantt.options.autoBgColor[this.task.autoStatus || 0]
        }`,
        append_to: this.bar_group,
      })

      //animateSVG(this.$bar, 'width', 0, this.width)

      if (this.invalid) {
        this.$bar.classList.add('bar-invalid')
      }
    }

    draw_label() {
      createSVG('text', {
        x: this.x + this.width / 2,
        y: this.y + this.height / 3,
        innerHTML: this.task.name,
        class: 'bar-label',
        append_to: this.bar_group,
      })
      createSVG('text', {
        x: this.x + this.width / 2,
        y: this.y + (this.height * 2) / 3,
        innerHTML: this.task.production,
        class: 'bar-label',
        append_to: this.bar_group,
      })

      // labels get BBox in the next tick
      requestAnimationFrame(() => this.update_label_position())
    }

    draw_resize_handles() {
      if (this.invalid) return

      const bar = this.$bar
      const handle_width = 8

      createSVG('rect', {
        x: bar.getX() + bar.getWidth() - 9,
        y: bar.getY() + 1,
        width: handle_width,
        height: this.height - 2,
        rx: this.corner_radius,
        ry: this.corner_radius,
        class: 'handle right',
        append_to: this.handle_group,
      })

      createSVG('rect', {
        x: bar.getX() + 1,
        y: bar.getY() + 1,
        width: handle_width,
        height: this.height - 2,
        rx: this.corner_radius,
        ry: this.corner_radius,
        class: 'handle left',
        append_to: this.handle_group,
      })
    }

    bind() {
      if (this.invalid) return
      this.setup_click_event()
    }

    setup_click_event() {
      $.on(
        this.group,
        this.gantt.options.popup_trigger,
        '.bar-wrapper',
        (e) => {
          this.index = this.oldIndex
          this.show_popup()
          this.gantt.unselect_all()
          this.group.classList.add('active')
        }
      )

      //双击选中任务
      $.on(this.group, 'dblclick', '.bar-wrapper', (e, element) => {
        this.gantt.hide_popup()
        this.group.classList.add('selected')
        this.gantt.selected_task = this.task
      })

      //选中任务之后，鼠标移到别的任务上才显示左右锚点
      $.on(this.group, 'mouseenter', '.bar-wrapper', (e, element) => {
        if (
          this.gantt.selected_task != null &&
          this.gantt.selected_task.id != this.task.id
        ) {
          this.handle_group.querySelector('.handle.left').classList.add('show')
          this.handle_group.querySelector('.handle.right').classList.add('show')
        }
      })

      $.on(this.group, 'mouseleave', '.bar-wrapper', (e, element) => {
        this.handle_group.querySelector('.handle.left').classList.remove('show')
        this.handle_group
          .querySelector('.handle.right')
          .classList.remove('show')
      })

      $.on(this.group, 'click', '.handle', (e, element) => {
        if (!this.gantt.selected_task) return
        let from_task = this.gantt.selected_task
        this.gantt.selected_task = null
        this.gantt.options.move_task(
          from_task,
          this.task,
          element.classList.contains('left')
        )
        this.gantt.hide_popup()
        this.gantt.unselect_all()
      })

      // 新增右击展示菜单
      $.on(this.group, 'contextmenu', '.bar-wrapper', (e, element) => {
        this.gantt.unselect_all()
        this.group.classList.add('rightActive')

        this.gantt.$barMenuBox.style.display = 'block'
        this.gantt.$barMenuBox.style.left = e.offsetX + 'px'

        let styleTop = 0
        if (
          e.offsetY + (this.gantt.options.barMenuList.length - 1) * 28 >=
          this.gantt.$container.clientHeight
        ) {
          styleTop =
            (this.gantt.options.barMenuList.length - 1) * 28 -
            (this.gantt.$container.clientHeight - e.offsetY)
        }
        console.log(
          e.offsetY,
          (this.gantt.options.barMenuList.length - 1) * 28,
          this.gantt.$container.clientHeight,
          this.gantt.$container.clientHeight -
            ((this.gantt.options.barMenuList.length - 1) * 28 - styleTop),
          styleTop
        )
        if (styleTop) {
          this.gantt.$barMenuBox.style.top =
            e.offsetY -
            ((this.gantt.options.barMenuList.length - 1) * 28 - styleTop) +
            'px'
        } else {
          this.gantt.$barMenuBox.style.top = e.offsetY + 'px'
        }
      })
    }

    show_popup() {
      if (this.gantt.bar_being_dragged) return
      const start_date = date_utils.format(this.task._start, 'MM-DD HH:mm:ss')
      const end_date = date_utils.format(
        date_utils.add(this.task._end, -1, 'second'),
        'MM-DD HH:mm:ss'
      )
      let subtitle = ''
      for (let i = 0; i < this.gantt.options.popupList.length; i++) {
        subtitle += `<div class="popup-item"> <span>${
          this.gantt.options.popupList[i].value
        }</span><span>${
          this.tasks[this.index][this.gantt.options.popupList[i].key]
        }</span> </div>`
      }
      let title = `<i class="el-icon-arrow-left pointer"></i>
        <span>${this.tasks[this.index].name}</span>
        <i class="el-icon-arrow-right pointer"></i>`
      this.titleDom = document.createElement('div')
      this.titleDom.classList.add('flex')
      this.titleDom.classList.add('flex-row-sa')
      this.titleDom.classList.add('popup-title')
      this.titleDom.innerHTML = title
      // let titleDom = new DOMParser().parseFromString(title, 'text/html')
      this.bind_title_event()
      // const subtitle = start_date + ' - ' + end_date
      this.gantt.show_popup({
        target_element: this.$bar,
        title: this.titleDom,
        subtitle: subtitle,
        task: this.task,
      })
    }
    bind_title_event() {
      let dom = this.titleDom.querySelectorAll('i')
      dom.forEach((x, i) => {
        if (i == 0) {
          x.onclick = () => {
            if (this.index > 0) {
              this.index--
              this.show_popup()
            }
          }
        } else {
          x.onclick = () => {
            if (this.index < this.tasks.length) {
              this.index++
              this.show_popup()
            }
          }
        }
      })
    }
    date_changed() {
      let changed = false
      const { new_start_date, new_end_date } = this.compute_start_end_date()

      if (Number(this.task._start) !== Number(new_start_date)) {
        changed = true
        this.task._start = new_start_date
      }

      if (Number(this.task._end) !== Number(new_end_date)) {
        changed = true
        this.task._end = new_end_date
      }

      if (!changed) return

      this.gantt.trigger_event('date_change', [
        this.task,
        new_start_date,
        date_utils.add(new_end_date, -1, 'second'),
      ])
    }

    compute_start_end_date() {
      const bar = this.$bar
      const x_in_units = bar.getX() / this.gantt.options.column_width
      const new_start_date = date_utils.add(
        this.gantt.gantt_start,
        x_in_units * this.gantt.options.step,
        'hour'
      )
      const width_in_units = bar.getWidth() / this.gantt.options.column_width
      const new_end_date = date_utils.add(
        new_start_date,
        width_in_units * this.gantt.options.step,
        'hour'
      )
      return { new_start_date, new_end_date }
    }

    compute_x() {
      const { step, column_width } = this.gantt.options
      const task_start = this.task._start
      const gantt_start = this.gantt.gantt_start
      const diff = date_utils.diff(task_start, gantt_start, 'hour')
      let x = (Math.round((diff * 100000000) / step) / 100000000) * column_width
      if (this.gantt.view_is('Month')) {
        const diff = date_utils.diff(task_start, gantt_start, 'day')
        x = (diff * column_width) / 30
      }
      return x
    }

    compute_y() {
      return (
        this.gantt.options.padding / 2 +
        this.task._index * (this.height + this.gantt.options.padding)
      )
    }

    update_attr(element, attr, value) {
      value = +value
      if (!isNaN(value)) {
        element.setAttribute(attr, value)
      }
      return element
    }

    update_label_position() {
      const bar = this.$bar,
        label = this.group.querySelectorAll('.bar-label')
      label.forEach((x) => {
        if (x.getBBox().width > bar.getWidth()) {
          //label超出bar宽度则移除
          x.remove()
        } else {
          x.classList.remove('big')
          x.setAttribute('x', bar.getX() + bar.getWidth() / 2)
        }
      })
    }

    update_handle_position() {
      const bar = this.$bar
      this.handle_group
        .querySelector('.handle.left')
        .setAttribute('x', bar.getX() + 1)
      this.handle_group
        .querySelector('.handle.right')
        .setAttribute('x', bar.getEndX() - 9)
    }
  }

  class Popup {
    constructor(parent, custom_html) {
      this.parent = parent
      this.custom_html = custom_html
      this.make()
    }

    make() {
      this.parent.innerHTML = `
            <div class="title"></div>
            <div class="subtitle"></div>
            <div class="pointer"></div>
        `
      this.hide()

      this.title = this.parent.querySelector('.title')
      this.subtitle = this.parent.querySelector('.subtitle')
      this.pointer = this.parent.querySelector('.pointer')
    }

    show(options) {
      if (!options.target_element) {
        throw new Error('target_element is required to show popup')
      }
      if (!options.position) {
        options.position = 'left'
      }
      const target_element = options.target_element

      if (this.custom_html) {
        let html = this.custom_html(options.task)
        html += '<div class="pointer"></div>'
        this.parent.innerHTML = html
        this.pointer = this.parent.querySelector('.pointer')
      } else {
        // set data
        while (this.title.firstChild) {
          this.title.firstChild.remove()
        }
        this.title.appendChild(options.title)
        this.subtitle.innerHTML = options.subtitle
        this.parent.style.width = this.parent.clientWidth + 'px'
      }
      // display导致元素拿取不到改元素的clientHeight
      this.parent.style.display = ''
      // set position
      let position_meta
      if (target_element instanceof HTMLElement) {
        position_meta = target_element.getBoundingClientRect()
      } else if (target_element instanceof SVGElement) {
        position_meta = options.target_element.getBBox()
      }
      let popupTitleHeight =
        this.title.clientHeight + this.subtitle.clientHeight
      let $container =
        target_element.parentElement.parentElement.parentElement.parentElement
          .parentElement
      if (options.position === 'left') {
        this.parent.style.left =
          position_meta.x + (position_meta.width + 10) + 'px'
        let styleY = position_meta.y
        if (
          parseInt(target_element.getAttribute('y')) + popupTitleHeight >=
          $container.clientHeight - 50
        ) {
          styleY =
            position_meta.y -
            100 -
            (parseInt(target_element.getAttribute('y')) +
              popupTitleHeight -
              $container.clientHeight)
        }
        this.parent.style.top = styleY + 65 + 4 + 'px'

        this.pointer.style.transform = 'rotateZ(90deg)'
        this.pointer.style.left = '-7px'
        this.pointer.style.top = '2px'
      }

      // show
      this.parent.style.opacity = 1

      this.parent.style.width = 'auto'
    }

    hide() {
      this.parent.style.opacity = 0
      this.parent.style.display = 'none'
      this.parent.style.left = 0
    }
  }

  class Gantt {
    constructor(wrapper, tasks, rows, options) {
      this.setup_options(options)
      this.setup_wrapper(wrapper)
      this.setup_tasks(tasks, rows)
      // initialize with default view mode
      this.change_view_mode()
      this.bind_events()
    }

    setup_wrapper(element) {
      let svg_element, wrapper_element, svg_date_element

      // CSS Selector is passed
      if (typeof element === 'string') {
        element = document.querySelector(element)
      }
      // get the SVGElement
      if (element instanceof HTMLElement) {
        wrapper_element = element
        svg_element = element.querySelectorAll('svg')[0]
        svg_date_element = element.querySelectorAll('svg')[1]
      } else if (element instanceof SVGElement) {
        svg_element = element
      } else {
        throw new TypeError(
          'Frappé Gantt only supports usage of a string CSS selector,' +
            " HTML DOM element or SVG DOM element for the 'element' parameter"
        )
      }
      wrapper_element.style.display = 'flex'
      // wrapper_element.style.alignItems = 'center'
      // svg element
      if (!svg_element) {
        // create it
        this.$svg = createSVG('svg', {
          append_to: wrapper_element,
          class: 'gantt',
        })
      } else {
        this.$svg = svg_element
        this.$svg.classList.add('gantt')
      }

      if (!svg_date_element) {
        // create it
        this.$svgTop = createSVG('svg', {
          append_to: wrapper_element,
          class: 'gantt header-fixed',
        })
      } else {
        this.$svgTop = svg_date_element
        this.$svgTop.classList.add('gantt header-fixed')
      }

      // menu element
      this.$menuBox = document.createElement('div')
      this.$menuBox.classList.add('menuBox')
      if (this.options.menuList.length) {
        for (let i = 0; i < this.options.menuList.length; i++) {
          let liDom = document.createElement('div')
          liDom.innerHTML = this.options.menuList[i]
          liDom.addEventListener('click', (e) => {
            if (
              this.options.menuClickHandle &&
              this.options.menuClickHandle instanceof Function
            ) {
              this.options.menuClickHandle(i, 'menu', e)
            }
          })
          this.$menuBox.appendChild(liDom)
        }
      }
      // barmenu element
      this.$barMenuBox = document.createElement('div')
      this.$barMenuBox.classList.add('barMenuBox')
      if (this.options.barMenuList.length) {
        for (let i = 0; i < this.options.barMenuList.length; i++) {
          let liDom2 = document.createElement('div')
          liDom2.innerHTML = this.options.barMenuList[i]
          liDom2.addEventListener('click', (e) => {
            if (
              this.options.menuClickHandle &&
              this.options.menuClickHandle instanceof Function
            ) {
              this.options.menuClickHandle(i, 'barMenu', e)
            }
          })
          this.$barMenuBox.appendChild(liDom2)
        }
      }

      // wrapper element
      this.$container = document.createElement('div')
      this.$container.classList.add('gantt-container')
      this.$left = document.createElement('div')
      this.$left.classList.add('gantt-left')
      this.$left.style.height = this.$container.style.height
      this.$leftView = document.createElement('div')
      this.$leftView.classList.add('gantt-left-view')

      this.$left.appendChild(this.$leftView)

      const parent_element = this.$svg.parentElement
      parent_element.appendChild(this.$left)
      parent_element.appendChild(this.$container)

      // this.$containerTop = document.createElement('div')
      // this.$containerTop.classList.add('gantt-container')
      // this.$containerTop.classList.add('gantt-container-fixed')

      // const parent_element2 = this.$svgTop.parentElement
      // parent_element2.appendChild(this.$containerTop)
      this.$container.appendChild(this.$svgTop)
      this.$container.appendChild(this.$svg)
      // this.$containerTop.appendChild(this.$svgTop)

      // popup wrapper
      this.popup_wrapper = document.createElement('div')
      this.popup_wrapper.classList.add('popup-wrapper')
      this.$container.appendChild(this.popup_wrapper)

      // 右击菜单
      this.$container.appendChild(this.$menuBox)
      this.$container.appendChild(this.$barMenuBox)
    }

    setup_options(options) {
      const default_options = {
        header_height: 50,
        column_width: 30,
        step: 24,
        bar_height: 40,
        bar_corner_radius: 3,
        padding: 18,
        view_mode: 'Day',
        date_format: 'YYYY-MM-DD',
        popup_trigger: 'click',
        custom_popup_html: null,
        leftItemWidth: 200,
        leftItemCss: 'leftItemCss',
        move_data: function () {},
      }
      this.options = Object.assign({}, default_options, options)
    }

    setup_tasks(tasks, rows) {
      this.rows = rows

      // prepare tasks
      this.tasks = tasks.map((task, i) => {
        // convert to Date objects
        task._start = date_utils.parse(task.start)
        task._end = date_utils.parse(task.end)

        // make task invalid if duration too large
        if (date_utils.diff(task._end, task._start, 'year') > 10) {
          task.end = null
        }

        // calc index
        if (rows) {
          var row_id = rows.findIndex((x) => x.id == (task.row_id || 'none'))
          if (row_id == -1) {
            rows.push({ id: '--none--', name: 'none' })
            task._index = rows.length - 1
          } else {
            task._index = row_id
          }
        }

        // invalid dates
        if (!task.start && !task.end) {
          const today = date_utils.today()
          task._start = today
          task._end = date_utils.add(today, 2, 'day')
        }

        if (!task.start && task.end) {
          task._start = date_utils.add(task._end, -2, 'day')
        }

        if (task.start && !task.end) {
          task._end = date_utils.add(task._start, 2, 'day')
        }

        // if hours is not set, assume the last day is full day
        // e.g: 2018-09-09 becomes 2018-09-09 23:59:59
        const task_end_values = date_utils.get_date_values(task._end)
        if (task_end_values.slice(3).every((d) => d === 0)) {
          task._end = date_utils.add(task._end, 24, 'hour')
        }

        // invalid flag
        if (!task.start || !task.end) {
          task.invalid = true
        }

        // dependencies
        if (typeof task.dependencies === 'string' || !task.dependencies) {
          let deps = []
          if (task.dependencies) {
            deps = task.dependencies
              .split(',')
              .map((d) => d.trim())
              .filter((d) => d)
          }
          task.dependencies = deps
        }

        // uids
        if (!task.id) {
          task.id = generate_id(task)
        }

        return task
      })

      this.setup_dependencies()
    }

    setup_dependencies() {
      this.dependency_map = {}
      for (let t of this.tasks) {
        for (let d of t.dependencies) {
          this.dependency_map[d] = this.dependency_map[d] || []
          this.dependency_map[d].push(t.id)
        }
      }
    }

    //全部刷新 重新渲染
    refresh(tasks, rows, rowIds) {
      this.setup_tasks(tasks, rows)
      this.setup_dates()
      this.render_leftRows(rows)
      this.render()
    }
    // 渲染svg左侧容器
    render_leftRows(rows) {
      while (this.$leftView.firstChild) {
        this.$leftView.removeChild(this.$leftView.firstChild)
      }
      rows.map((x) => {
        let wrap = document.createElement('div')
        wrap.classList.add(this.options.leftItemCss)
        wrap.innerHTML = x.id
        wrap.style.height =
          this.options.bar_height + this.options.padding + 'px'
        wrap.style.lineHeight =
          this.options.bar_height + this.options.padding + 'px'
        this.$leftView.appendChild(wrap)
      })
    }

    render() {
      this.clear()
      this.setup_layers()
      this.make_grid()
      this.make_dates()
      this.make_bars()
      this.set_width()
      this.set_scroll_position()
    }

    refresh_rows(tasks, rowIds) {
      this.setup_tasks(tasks, this.rows)
      this.clear_row_line(rowIds)
      this.make_bars_by_row(rowIds)
    }

    change_view_mode(mode = this.options.view_mode) {
      this.update_view_scale(mode)
      this.setup_dates()
      // 增加渲染左侧list数组
      this.render_leftRows(this.rows)
      this.render()
    }

    update_view_scale(view_mode) {
      this.options.view_mode = view_mode
      if (view_mode === VIEW_MODE.DAY) {
        this.options.step = 24
        this.options.column_width = 38
      } else if (view_mode === VIEW_MODE.HALF_DAY) {
        this.options.step = 24 / 2
        this.options.column_width = 38
      } else if (view_mode === VIEW_MODE.QUARTER_DAY) {
        this.options.step = 24 / 4
        this.options.column_width = 38
      } else if (view_mode === VIEW_MODE.WEEK) {
        this.options.step = 24 * 7
        this.options.column_width = 140
      } else if (view_mode === VIEW_MODE.MONTH) {
        this.options.step = 24 * 30
        this.options.column_width = 120
      } else if (view_mode === VIEW_MODE.YEAR) {
        this.options.step = 24 * 365
        this.options.column_width = 120
      }
    }

    setup_dates() {
      this.setup_gantt_dates()
      this.setup_date_values()
    }

    setup_gantt_dates() {
      this.gantt_start = this.gantt_end = null

      for (let task of this.tasks) {
        // set global start and end date
        if (!this.gantt_start || task._start < this.gantt_start) {
          this.gantt_start = task._start
        }
        if (!this.gantt_end || task._end > this.gantt_end) {
          this.gantt_end = task._end
        }
      }

      this.gantt_start = date_utils.start_of(this.gantt_start, 'day')
      this.gantt_end = date_utils.start_of(this.gantt_end, 'day')

      // add date padding on both sides
      if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY])) {
        this.gantt_start = date_utils.add(this.gantt_start, 0, 'day')
        this.gantt_end = date_utils.add(this.gantt_end, 7, 'day')
      } else if (this.view_is(VIEW_MODE.MONTH)) {
        this.gantt_start = date_utils.start_of(this.gantt_start, 'year')
        this.gantt_end = date_utils.add(this.gantt_end, 1, 'year')
      } else if (this.view_is(VIEW_MODE.YEAR)) {
        this.gantt_start = date_utils.add(this.gantt_start, -2, 'year')
        this.gantt_end = date_utils.add(this.gantt_end, 2, 'year')
      } else {
        this.gantt_start = date_utils.add(this.gantt_start, -1, 'month')
        this.gantt_end = date_utils.add(this.gantt_end, 1, 'month')
      }
    }

    setup_date_values() {
      this.dates = []
      let cur_date = null

      while (cur_date === null || cur_date < this.gantt_end) {
        if (!cur_date) {
          cur_date = date_utils.clone(this.gantt_start)
        } else {
          if (this.view_is(VIEW_MODE.YEAR)) {
            cur_date = date_utils.add(cur_date, 1, 'year')
          } else if (this.view_is(VIEW_MODE.MONTH)) {
            cur_date = date_utils.add(cur_date, 1, 'month')
          } else {
            cur_date = date_utils.add(cur_date, this.options.step, 'hour')
          }
        }
        this.dates.push(cur_date)
      }
    }

    bind_events() {
      this.bind_grid_click()
      this.$left.onmousewheel = function (e) {
        e.preventDefault()
      }
      this.$container.onscroll = (e) => {
        this.options.vueSetScroll(e)
        this.$leftView.scrollTop = e.target.scrollTop
      }
      this.$container.oncontextmenu = (e) => {
        e = e || window.event
        // 阻止浏览器的默认行为
        if (e && e.preventDefault) {
          e.preventDefault() //防止浏览器默认行为(W3C)
          e.stopPropagation()
        } else {
          window.event.returnValue = false //IE中阻止浏览器行为
        }
        if (e.path.length <= 15) {
          // this.unselect_all()
          // 清除bar的右键菜单展示
          this.$barMenuBox.style.display = 'none'
          // 事件被触发时，显示右键菜单
          this.$menuBox.style.display = 'block'
          // 位置定位在当前鼠标的位置
          this.$menuBox.style.left =
            e.offsetX + this.$container.scrollLeft + 'px'
          let y = 0
          if (
            e.offsetY + this.options.menuList.length * this.options.bar_height >
            this.$container.clientHeight
          ) {
            y =
              e.offsetY +
              this.options.menuList.length * this.options.bar_height -
              this.$container.clientHeight
          }
          this.$menuBox.style.top =
            e.offsetY - y + this.$container.scrollTop + 70 + 'px'
          this.$left.style.height = this.$container.style.height
        } else {
          this.$menuBox.style.display = 'none'
        }
      }
      this.$container.parentElement.addEventListener('click', (e) => {
        this.$menuBox.style.display = 'none'
        this.$barMenuBox.style.display = 'none'
        console.log(e.path.length)
        if (e.path.length < 16 && e.path.length != 15) {
          this.unselect_all()
          this.hide_popup()
        }
      })
    }

    //全部重新渲染
    render() {
      this.clear()
      this.setup_layers()
      this.make_grid()
      this.make_dates()
      this.make_bars()
      this.set_width()
      this.set_scroll_position()
    }

    setup_layers() {
      this.layers = {}
      const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details']
      // make group layers
      for (let layer of layers) {
        this.layers[layer] = createSVG('g', {
          class: layer,
          append_to: this.$svg,
        })
      }
      this.layers['date'] = createSVG('g', {
        class: 'date',
        append_to: this.$svgTop,
      })
    }

    make_grid() {
      this.make_grid_background()
      this.make_grid_rows()
      // this.make_grid_header()
      this.make_grid_ticks()
      this.make_grid_highlights()
    }

    make_grid_background() {
      const grid_width = this.dates.length * this.options.column_width
      const grid_height =
        // this.options.header_height +
        this.options.padding +
        (this.options.bar_height + this.options.padding) * this.rows.length

      this.$gridBackground = createSVG('rect', {
        x: 0,
        y: 0,
        width: grid_width,
        height: grid_height,
        class: 'grid-background',
        append_to: this.layers.grid,
      })

      $.attr(this.$svg, {
        height: grid_height,
        width: '100%',
      })
    }

    make_grid_rows() {
      const rows_layer = createSVG('g', { append_to: this.layers.grid })
      const lines_layer = createSVG('g', { append_to: this.layers.grid })

      const row_width = this.dates.length * this.options.column_width
      const row_height = this.options.bar_height + this.options.padding

      // let row_y = row_height + this.options.padding / 2
      let row_y = 0

      for (let row of this.rows) {
        createSVG('rect', {
          x: 0,
          y: row_y,
          width: row_width,
          height: row_height,
          class: 'grid-row',
          append_to: rows_layer,
        })
        // createSVG('text', {
        //   x: 30,
        //   y: row_y + row_height / 1.5,
        //   innerHTML: row.name,
        //   class: 'lower-text',
        //   append_to: rows_layer,
        // })
        createSVG('line', {
          x1: 0,
          y1: row_y + row_height,
          x2: row_width,
          y2: row_y + row_height,
          class: 'row-line',
          append_to: lines_layer,
        })
        row_y += this.options.bar_height + this.options.padding
      }
    }

    make_grid_header() {
      const header_width = this.dates.length * this.options.column_width
      const header_height = this.options.header_height + 10
      createSVG('rect', {
        x: 0,
        y: 0,
        width: header_width,
        height: header_height,
        class: 'grid-header',
        append_to: this.layers.grid,
      })
    }

    make_grid_ticks() {
      let tick_x = 0
      let tick_y = this.options.header_height + this.options.padding / 2
      let tick_height =
        (this.options.bar_height + this.options.padding) * this.rows.length

      for (let date of this.dates) {
        let tick_class = 'tick'
        // thick tick for monday
        if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {
          tick_class += ' thick'
        }
        // thick tick for first week
        if (
          this.view_is(VIEW_MODE.WEEK) &&
          date.getDate() >= 1 &&
          date.getDate() < 8
        ) {
          tick_class += ' thick'
        }
        // thick ticks for quarters
        if (this.view_is(VIEW_MODE.MONTH) && (date.getMonth() + 1) % 3 === 0) {
          tick_class += ' thick'
        }

        createSVG('path', {
          d: `M ${tick_x} ${tick_y} v ${tick_height}`,
          class: tick_class,
          append_to: this.layers.grid,
        })

        if (this.view_is(VIEW_MODE.MONTH)) {
          tick_x +=
            (date_utils.get_days_in_month(date) * this.options.column_width) /
            30
        } else {
          tick_x += this.options.column_width
        }
      }
    }

    make_grid_highlights() {
      // highlight today's date
      if (this.view_is(VIEW_MODE.DAY)) {
        const x = 0
        const y = 0
        const width = this.options.column_width
        const height =
          (this.options.bar_height + this.options.padding) * this.rows.length +
          this.options.header_height +
          this.options.padding / 2
        createSVG('rect', {
          x,
          y,
          width,
          height,
          class: 'today-highlight',
          append_to: this.layers.grid,
        })
      }
    }

    make_dates() {
      for (let date of this.get_dates_to_draw()) {
        createSVG('text', {
          x: date.upper_x,
          y: date.lower_y,
          innerHTML: date.lower_text,
          class: 'lower-text',
          append_to: this.layers.date,
        })
        createSVG('text', {
          x: date.upper_x,
          y: date.lower_y + 16,
          innerHTML: '|',
          class: 'lower-text-s',
          append_to: this.layers.date,
        })

        if (date.upper_text) {
          const $upper_text = createSVG('text', {
            x: date.upper_x,
            y: date.upper_y,
            innerHTML: date.upper_text,
            class: 'upper-text',
            append_to: this.layers.date,
          })

          // remove out-of-bound dates
          if ($upper_text.getBBox().x2 > this.layers.grid.getBBox().width) {
            $upper_text.remove()
          }
        }
      }
    }

    get_dates_to_draw() {
      let last_date = null
      const dates = this.dates.map((date, i) => {
        const d = this.get_date_info(date, last_date, i)
        last_date = date
        return d
      })
      return dates
    }

    get_date_info(date, last_date, i) {
      if (!last_date) {
        last_date = date_utils.add(date, 1, 'year')
      }
      const date_text = {
        'Quarter Day_lower': date_utils.format(date, 'HH'),
        'Half Day_lower': date_utils.format(date, 'HH'),
        Day_lower:
          date.getDate() !== last_date.getDate()
            ? date_utils.format(date, 'DD')
            : '',
        Week_lower:
          date.getMonth() !== last_date.getMonth()
            ? date_utils.format(date, 'MM-DD')
            : date_utils.format(date, 'DD'),
        Month_lower: date_utils.format(date, 'MM'),
        Year_lower: date_utils.format(date, 'YYYY'),
        'Quarter Day_upper':
          date.getDate() !== last_date.getDate()
            ? date_utils.format(date, 'MM-DD')
            : '',
        'Half Day_upper':
          date.getDate() !== last_date.getDate()
            ? date_utils.format(date, 'MM-DD')
            : '',
        Day_upper:
          date.getMonth() !== last_date.getMonth()
            ? date_utils.format(date, 'MM')
            : '',
        Week_upper:
          date.getMonth() !== last_date.getMonth()
            ? date_utils.format(date, 'MM')
            : '',
        Month_upper:
          date.getFullYear() !== last_date.getFullYear()
            ? date_utils.format(date, 'YYYY')
            : '',
        Year_upper:
          date.getFullYear() !== last_date.getFullYear()
            ? date_utils.format(date, 'YYYY')
            : '',
      }

      const base_pos = {
        x: i * this.options.column_width,
        lower_y: this.options.header_height,
        upper_y: this.options.header_height - 25,
      }

      const x_pos = {
        'Quarter Day_lower': (this.options.column_width * 4) / 2,
        'Quarter Day_upper': 0,
        'Half Day_lower': (this.options.column_width * 2) / 2,
        'Half Day_upper': 0,
        Day_lower: this.options.column_width / 2,
        Day_upper: (this.options.column_width * 30) / 2,
        Week_lower: 0,
        Week_upper: (this.options.column_width * 4) / 2,
        Month_lower: this.options.column_width / 2,
        Month_upper: (this.options.column_width * 12) / 2,
        Year_lower: this.options.column_width / 2,
        Year_upper: (this.options.column_width * 30) / 2,
      }

      return {
        upper_text: date_text[`${this.options.view_mode}_upper`],
        lower_text: date_text[`${this.options.view_mode}_lower`],
        upper_x: base_pos.x + x_pos[`${this.options.view_mode}_upper`],
        upper_y: base_pos.upper_y,
        lower_x: base_pos.x + x_pos[`${this.options.view_mode}_lower`],
        lower_y: base_pos.lower_y,
      }
    }

    make_bars() {
      this.bars = this.tasks.map((task) => {
        let rowtasks = this.tasks.filter((x) => x.row_id == task.row_id)
        let index = rowtasks.findIndex((x) => task.id == x.id)
        const bar = new Bar(this, task, index, rowtasks)
        this.layers.bar.appendChild(bar.group)
        return bar
      })
    }

    make_bars_by_row(rowIds) {
      this.tasks
        .filter((x) => rowIds.includes(x.row_id))
        .map((task) => {
          let rowtasks = this.tasks.filter((x) => x.row_id == task.row_id)
          let index = rowtasks.findIndex((x) => task.id == x.id)
          const bar = new Bar(this, task, index, rowtasks)
          this.layers.bar.appendChild(bar.group)
        })
    }

    // 清除某行的所有任务
    clear_row_line(rowIds) {
      let clearBar = [],
        clearBarWrapper = [],
        clearBarIds = []
      clearBar = this.tasks.filter((x) => rowIds.includes(x.row_id))
      clearBar.map((x) => clearBarIds.push(x.id))
      let barWrappers = document.querySelectorAll('.bar-wrapper')
      for (let i = 0; i < barWrappers.length; i++) {
        if (clearBarIds.includes(barWrappers[i].getAttribute('data-id')))
          clearBarWrapper.push(barWrappers[i])
      }
      clearBarWrapper.map((x) => {
        this.layers.bar.removeChild(x)
      })
    }

    set_width() {
      const cur_width = this.$svg.getBoundingClientRect().width
      const actual_width = this.$svg
        .querySelector('.grid .grid-row')
        .getAttribute('width')
      if (cur_width < actual_width) {
        this.$svg.setAttribute('width', actual_width)
        this.$svgTop.setAttribute('width', actual_width)
        this.$svgTop.setAttribute('height', 65)
      }
    }

    set_scroll_position() {
      const parent_element = this.$svg.parentElement
      if (!parent_element) return
      const hours_before_first_task = date_utils.diff(
        this.get_oldest_starting_date(),
        this.gantt_start,
        'hour'
      )
      const scroll_pos =
        (hours_before_first_task / this.options.step) *
          this.options.column_width -
        this.options.column_width
      parent_element.scrollLeft = scroll_pos
    }

    bind_grid_click() {
      $.on(
        this.$svg,
        this.options.popup_trigger,
        '.grid-row, .grid-header',
        () => {
          this.unselect_all()
          this.hide_popup()
        }
      )

      let gridBackgroundIsMousedown = false,
        x = 0,
        y = 0,
        scrollTop = 0,
        scrollLeft = 0
      $.on(this.$container, 'mousedown', 'svg', (e) => {
        gridBackgroundIsMousedown = true
        x = e.x
        y = e.y
        scrollTop = this.$container.scrollTop
        scrollLeft = this.$container.scrollLeft
      })
      $.on(this.$container, 'mousemove', 'svg', (e) => {
        if (gridBackgroundIsMousedown) {
          this.$container.scrollTop = y - e.y + scrollTop
          this.$container.scrollLeft = x - e.x + scrollLeft
        }
      })
      $.on(this.$container, 'mouseup', 'svg', (e) => {
        gridBackgroundIsMousedown = false
        x = 0
        y = 0
      })
      $.on(this.$container, 'mouseleave', (e) => {
        gridBackgroundIsMousedown = false
        x = 0
        y = 0
      })
    }

    unselect_all() {
      ;[...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => {
        el.classList.remove('active')
        el.classList.remove('selected')
        el.classList.remove('rightActive')
      })
    }

    view_is(modes) {
      if (typeof modes === 'string') {
        return this.options.view_mode === modes
      }

      if (Array.isArray(modes)) {
        return modes.some((mode) => this.options.view_mode === mode)
      }

      return false
    }

    get_task(id) {
      return this.tasks.find((task) => {
        return task.id === id
      })
    }

    get_bar(id) {
      return this.bars.find((bar) => {
        return bar.task.id === id
      })
    }

    show_popup(options) {
      if (!this.popup) {
        this.popup = new Popup(
          this.popup_wrapper,
          this.options.custom_popup_html
        )
      }
      this.popup.show(options)
    }

    hide_popup() {
      this.popup && this.popup.hide()
    }

    trigger_event(event, args) {
      if (this.options['on_' + event]) {
        this.options['on_' + event].apply(null, args)
      }
    }

    get_oldest_starting_date() {
      return this.tasks
        .map((task) => task._start)
        .reduce((prev_date, cur_date) =>
          cur_date <= prev_date ? cur_date : prev_date
        )
    }

    clear() {
      this.$svg.innerHTML = ''
      this.$svgTop.innerHTML = ''
    }
  }

  Gantt.VIEW_MODE = VIEW_MODE

  function generate_id(task) {
    return task.name + '_' + Math.random().toString(36).slice(2, 12)
  }

  return Gantt
})()
export default Gantt
